Skip to content

Instantly share code, notes, and snippets.

@nesevis
Last active January 20, 2024 22:27
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save nesevis/295018ed6669734ca7c0a0345732339e to your computer and use it in GitHub Desktop.
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.
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