Last active
January 20, 2024 22:27
-
-
Save nesevis/295018ed6669734ca7c0a0345732339e to your computer and use it in GitHub Desktop.
This represents a proof of concept for how to parse and output the binary format the Polyscience Control Freak cooker uses.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
open System.IO | |
[<AutoOpen>] | |
module ControlFreak = | |
type Timer = { | |
hours: int; | |
minutes: int; | |
seconds: int | |
} | |
type PowerLevel = | |
| Low | |
| Medium | |
| High | |
| Max | |
type TimerStart = | |
| AtBeginning | |
| AtSetTemperature | |
| AtPrompt | |
type TimerAfter = | |
| ContinueCooking | |
| StopCooking | |
| KeepWarm | |
| Repeat | |
type Program = { | |
name: string; // 1-20 characters | |
temp: int; // 30-250c | |
power: PowerLevel; | |
timer: Timer; // 5 secs to 72 hrs | |
timerStart: TimerStart; | |
timerAfter: TimerAfter; | |
} | |
// Constants | |
let maxF = 482 | |
let programFileSize = 8192 // bytes | |
let emptyByte = 255 // 0xff | |
let nullByte = 0 // 0x00 | |
// These values exist on recipes with names shorter than 6 characters | |
// Overwritten eventually by the name. | |
// The five last ASCII characters spell mk 01 | |
let minName = [|0;0;109;112;32;48;49|] | |
// Convenience functions | |
let isBitSet b i = b &&& (1 <<< i) <> 0 | |
let setBit b i = b ||| (1 <<< i) | |
let padRight len v arr = Array.append arr (Array.create (len - Array.length arr) v) | |
let convertCtoF temp = ((9.0 / 5.0) * (temp |> float)) + 32.0 |> int | |
let convertFtoC temp = (5.0 / 9.0) * ((temp |> float) - 32.0) |> int | |
let isValidChar b = | |
match b with | |
| 32 -> true // Space | |
| _ when b >= 48 && b <= 57 -> true // 0-9 | |
| _ when b >= 65 && b <= 90 -> true // A-Z | |
| _ when b >= 97 && b <= 122 -> true // a-z | |
| _ -> false | |
// Parsing methods | |
let nameFromBytes (bs: int[]): string = | |
bs | |
|> Array.takeWhile (fun i -> i <> nullByte) | |
|> Array.filter isValidChar | |
|> Array.map byte | |
|> Array.map char | |
|> System.String | |
let bytesFromName (name: string) : int[] = | |
name | |
|> Array.ofSeq | |
|> Array.map int | |
|> Array.filter isValidChar | |
|> padRight 20 nullByte | |
|> Array.zip (minName |> padRight 20 nullByte) | |
|> Array.map (fun t -> if snd t <> nullByte then snd t else fst t) | |
let temperatureFromBytes (bs: int[]) = bs.[0] + if (isBitSet bs.[1] 7) then 255 else 0 | |
let bytesFromTemperature temp = [| | |
temp % (maxF - 255 + 1); | |
(if temp > (maxF - 255) then (setBit 0 7) else 0) | |
|] | |
let timerDurationFromBytes (bs: int[]): Timer = { | |
hours = min bs.[0] (if bs.[1] > 0 || bs.[2] > 0 then 71 else 72); | |
minutes = min bs.[1] 59; | |
seconds = min bs.[2] 59; | |
} | |
let powerLevelFromByte b : PowerLevel = | |
let set = isBitSet b | |
match b with | |
| _ when not (set 3) && not (set 2) -> Low | |
| _ when not (set 3) && set 2 -> Medium | |
| _ when set 3 && not (set 2) -> High | |
| _ when set 3 && set 2 -> Max | |
| _ -> Low | |
let byteFromPowerLevel (l: PowerLevel) (b: int): int = | |
match l with | |
| Low -> b | |
| Medium -> setBit b 2 | |
| High -> setBit b 3 | |
| Max -> setBit (setBit b 2) 3 | |
let timerStartFromByte b: TimerStart = | |
let set = isBitSet b | |
match b with | |
| _ when not (set 1) && not (set 0) -> AtBeginning | |
| _ when not (set 1) && set 0 -> AtSetTemperature | |
| _ when set 1 && not (set 0) -> AtPrompt | |
| _ -> AtBeginning | |
let byteFromStartFrom (ts: TimerStart) (b: int) = | |
match ts with | |
| AtBeginning -> b | |
| AtSetTemperature -> setBit b 0 | |
| AtPrompt -> setBit b 1 | |
let timerAfterFromByte b: TimerAfter = | |
let set = isBitSet b | |
match b with | |
| _ when not (set 6) && not (set 5) -> ContinueCooking | |
| _ when not (set 6) && set 5 -> StopCooking | |
| _ when set 6 && not (set 5) -> KeepWarm | |
| _ when set 6 && set 5 -> Repeat | |
| _ -> StopCooking | |
let byteFromTimerAfter (ta: TimerAfter) (b: int) = | |
match ta with | |
| ContinueCooking -> b | |
| StopCooking -> setBit b 5 | |
| KeepWarm -> setBit b 6 | |
| Repeat -> setBit (setBit b 5) 6 | |
// Truncates the decimal sum of the preceding bytes | |
// to the sum's 8 least significant bits | |
let checksumFromBytes (bs: int[]) = bs |> Array.sum |> byte |> int | |
let programFromBytes (bs: int[]): Program = { | |
name = nameFromBytes bs.[0..19]; | |
temp = (min (temperatureFromBytes bs.[30..31]) maxF); | |
power = powerLevelFromByte bs.[31]; | |
timerStart = timerStartFromByte bs.[31]; | |
timerAfter = timerAfterFromByte bs.[31]; | |
timer = timerDurationFromBytes bs.[32..34]; | |
} | |
let bytesFromProgram p: byte[] = | |
let mutable bs = Array.create 36 0 | |
bs.[0..19] <- bytesFromName p.name | |
// Bytes 20-29 are unused | |
bs.[30..31] <- bytesFromTemperature p.temp //(min (convertCtoF p.temp) maxF) | |
bs.[31] <- byteFromPowerLevel p.power bs.[31] | |
bs.[31] <- byteFromStartFrom p.timerStart bs.[31] | |
bs.[31] <- byteFromTimerAfter p.timerAfter bs.[31] | |
bs.[32] <- min p.timer.hours (if p.timer.hours > 0 || p.timer.minutes > 0 then 71 else 72) | |
bs.[33] <- min p.timer.minutes 59 | |
bs.[34] <- min p.timer.seconds 59 | |
bs.[35] <- checksumFromBytes bs | |
bs |> Array.map byte | |
[<EntryPoint>] | |
let main argv = | |
// Input / Output | |
let input = "../CMC850.FA1" | |
let output = "../output.FA1" | |
// Parsing from FA1 | |
let programs = | |
File.ReadAllBytes(input) | |
|> Array.map int // Easier to work with the bytes as ints | |
|> Array.filter(fun b -> b <> emptyByte) // Get rid of empty bytes (0xff) | |
|> Array.chunkBySize 36 // Each recipe is 36 bytes | |
|> Array.map programFromBytes | |
// Exporting parsed Program records | |
let parsedOutput = | |
programs | |
|> Array.map bytesFromProgram | |
|> Array.concat // Flatten 2D array | |
|> padRight programFileSize (emptyByte |> byte) | |
let outputFile = if File.Exists(output) then File.OpenWrite(output) else File.Create(output) | |
let writer = new BinaryWriter(outputFile) | |
writer.Write parsedOutput | |
writer.Close() | |
0 // Return an integer exit code |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment